# 好比特套利策略
# 分为两个脚本，一个是好比特下单脚本，一个是比特币平衡脚本
# 当前脚本为 比特币平衡 脚本
# PS: 策略文件应该放到根目录，不然可能会出错

import markets
import config
import time
import sys
import random
from concurrent.futures import ThreadPoolExecutor
from operator import attrgetter
from lib.bmobHelper import BmobHelper

exchanges = []
init_btc = 0.0  # 程序运行前，比特币数量，在程序运行过程中，数量一般保持不变

total_cny = 0.0  # 当前的人民币数量和比特币数量
total_btc = 0.0

last_cny = 0.0  # 每轮操作前，人民币数量，每轮操作后都可能会变
last_btc = 0.0  # 上一轮操作结束后的比特币数量


def init_exchanges():
    exchange_names = config.exchanges
    global exchanges  # 哪里需要使用全局变量，哪里就声明一下
    for exchange_name in exchange_names:
        try:
            exec('import markets.' + exchange_name.lower())
            exchange = eval('markets.' + exchange_name.lower() + '.' + exchange_name + '()')
            exchanges.append(exchange)
            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
            print("%s: Init exchange %s." % (datetime, exchange_name))
        except Exception as e:
            print("%s exchange name is invalid, please check config.py" % exchange_name)
            print(e)


# decimal表示浮点数值，number表示保留小数位数；number只能为1，2，3或者4
def retainDigiNum(decimal, number):  # 保留N位小数，不四舍五入；最好别用round函数，有精度误差的问题，会产生特别长的浮点数
    if number == 1:
        return int(decimal * 10) / 10
    if number == 2:
        return int(decimal * 100) / 100
    if number == 3:
        return int(decimal * 1000) / 1000
    if number == 4:
        return int(decimal * 10000) / 10000
    if number == 5:
        return int(decimal * 100000) / 100000
    if number == 6:
        return int(decimal * 1000000) / 1000000


def isNomal():  # 判断交易所资产更新是否正常
    flag = True
    for exchange in exchanges:
        if (exchange.cny_free == 0) and (exchange.cny_frozen == 0) and (exchange.btc_free == 0) and \
                (exchange.btc_frozen == 0):
            flag = False
            break
    return flag


def cancelAll():  # 不撤销好比特的订单
    global exchanges
    for exchange in exchanges:
        if exchange.__class__.__name__ == 'Haobtc':
            continue
        exchange.cancel_all()
    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
    print("%s: Cancel all orders except Haobtc!" % datetime)


def updateCurrentDepth(exclude=[]):  # 参数为当前操作不更新的市场列表
    global exchanges
    try:
        threadpool = ThreadPoolExecutor(max_workers=10)
        for exchange in exchanges:
            if exchange.__class__.__name__ in exclude:
                continue
            threadpool.submit(exchange.update_depth)
        threadpool.shutdown(wait=True)
    except Exception as e:
        # 若深度更新失败，将所有交易所深度信息置为0
        for exchange in exchanges:
            exchange.bid = 0.0
            exchange.bid_volume = 0.0
            exchange.ask = 0.0
            exchange.ask_volume = 0.0
        print('UpdateCurrentDepth Failed!' + str(e))


def updateCurrentAsset(exclude=[]):
    global exchanges
    try:
        threadpool = ThreadPoolExecutor(max_workers=10)
        for exchange in exchanges:
            if exchange.__class__.__name__ in exclude:
                continue
            threadpool.submit(exchange.update_accountInfo)
        threadpool.shutdown(wait=True)
    except Exception as e:
        # 若资产更新失败，将所有交易所的资产信息都置为0
        for exchange in exchanges:
            exchange.btc_free = 0.0
            exchange.btc_frozen = 0.0
            exchange.cny_free = 0.0
            exchange.cny_frozen = 0.0
        print('UpdateCurrentAsset Failed!' + str(e))

    global total_cny, total_btc
    total_btc = 0.0
    total_cny = 0.0
    for exchange in exchanges:
        total_cny += exchange.cny_free
        total_cny += exchange.cny_frozen
        total_btc += exchange.btc_free
        total_btc += exchange.btc_frozen


def getProfit():  # 计算当前总资产更上一周期的差值，即利润
    global total_cny, last_cny, total_btc, last_btc, init_btc
    bid = 0
    for exchange in exchanges:
        if (exchange.ask == 0) or (exchange.bid == 0):
            continue
        bid = exchange.bid
        break

    updateCurrentAsset()
    if isNomal() and (bid != 0) and (abs((total_btc-last_btc)*bid+total_cny-last_cny) < init_btc * 20 * 1):
        # 交易所资产正常,并且参考价格不为零，且资产没有大的变动，才做利润统计
        return (total_cny - last_cny) + (total_btc - last_btc) * bid
    else:
        return 0


def balanceBtc(exclude=[]):  # 平衡一轮操作前后的BTC数量，保持BTC量前后一致
    global total_btc, total_cny, init_btc, last_btc, last_cny, exchanges

    # 获取当前比特币价格
    bid = 0
    for exchange in exchanges:
        if (exchange.ask == 0) or (exchange.bid == 0):
            continue
        bid = exchange.bid
        break

    updateCurrentAsset(exclude)
    if isNomal() and (bid != 0) and (abs((total_btc-last_btc)*bid+total_cny-last_cny) < init_btc * 20 * 1):
        # 交易所资产更新都成功即为正常
        # 并且当前与上一轮的总资产差值的绝对值小于阈值，此举是为了避免好比特的资产api更新异常而设
        diff = retainDigiNum(total_btc - init_btc, 4)
        temp = abs(diff)
        updateCurrentDepth()
        if diff <= -0.001:  # 假如不平衡量小于0.001，交易所都不能下单，故不处理
            record = BmobHelper()
            balance_exchanges = sorted(exchanges, key=attrgetter('ask'))
            for exchange in balance_exchanges:
                if exchange.__class__.__name__ == 'Haobtc':  # 不在Haobtc主动下单
                    continue
                if exchange.ask == 0:
                    continue
                if exchange.__class__.__name__ == 'Okcoin':
                    if temp < config.MinAmountOnce:
                        continue  # 如果需要平衡的量小于0.01，并且当前交易所为Okcoin，跳过不做
                    else:
                        temp = retainDigiNum(temp, 3)  # 如果小数位数大于3位，只保留3位

                if exchange.cny_free >= temp * (exchange.ask + config.SlidePrice):
                    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                    if exchange.buy(exchange.ask + config.SlidePrice, temp):
                        record.record_op(datetime, "enbalanceBTC", exchange.__class__.__name__, None,
                                         exchange.ask + config.SlidePrice, temp, None)
                        temp = 0
                if (exchange.cny_free < temp * (exchange.ask + config.SlidePrice)) and \
                        (retainDigiNum(exchange.cny_free / (exchange.ask + config.SlidePrice), 3) >= 0.01):
                    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                    if exchange.buy(exchange.ask + config.SlidePrice,
                                    retainDigiNum(exchange.cny_free / (exchange.ask + config.SlidePrice), 3)):
                        record.record_op(datetime, "enbalanceBTC", exchange.__class__.__name__, None,
                                         exchange.ask + config.SlidePrice,
                                         retainDigiNum(exchange.cny_free / (exchange.ask + config.SlidePrice), 3), None)
                        temp -= retainDigiNum(exchange.cny_free / (exchange.ask + config.SlidePrice), 3)
                if temp < 0.01:
                    break
            if temp >= 0.001:
                datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                print("%s: Exchanges(except Haobtc) has not enough RMB to balance. Should buy %f btc."
                      % (datetime, temp))

        if 0.001 <= diff:
            record = BmobHelper()
            balance_exchanges = sorted(exchanges, key=attrgetter('bid'), reverse=True)
            for exchange in balance_exchanges:
                if exchange.__class__.__name__ == 'Haobtc':
                    continue
                if exchange.bid == 0:
                    continue
                if exchange.__class__.__name__ == 'Okcoin':
                    if temp < config.MinAmountOnce:
                        continue  # 如果需要平衡的量小于0.01，并且当前交易所为Okcoin，跳过不做
                    else:
                        temp = retainDigiNum(temp, 3)  # 如果小数位数大于3位，只保留3位

                if exchange.btc_free >= temp:
                    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                    if exchange.sell(exchange.bid - config.SlidePrice, temp):
                        record.record_op(datetime, "enbalanceBTC", None, exchange.__class__.__name__,
                                         exchange.bid - config.SlidePrice, temp, None)
                        temp = 0
                if (exchange.btc_free < temp) and (exchange.btc_free >= 0.01):
                    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                    if exchange.sell(exchange.bid - config.SlidePrice, retainDigiNum(exchange.btc_free, 3)):
                        record.record_op(datetime, "enbalanceBTC", None, exchange.__class__.__name__,
                                         exchange.bid - config.SlidePrice, retainDigiNum(exchange.btc_free, 3), None)
                        temp -= retainDigiNum(exchange.btc_free, 3)
                if temp < 0.01:
                    break
            if temp >= 0.001:
                datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                print("%s: Exchanges has not enough BTC to balance. Should sell %f btc."
                      % (datetime, temp))
    else:
        datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
        print("%s: BalanceBTC's condition error." % datetime)


def statisticsProfit():  # 统计利润
    global exchanges, init_btc, total_btc, total_cny, last_cny, last_btc
    # 标记利润来源，0表示来源于被动策略，1表示来源于主动策略；初始为0
    flag = 0
    record = BmobHelper()

    if int(time.time()) % config.PeriodRecordProfit < config.TiggerTime:
        print('----------Profit----------')
        datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
        profit = retainDigiNum(getProfit(), 6)  # 此操作会更新资产信息

        if profit != 0:  # 如果利润值为0，上一次的比特币数量和人民币数量不变化；利润值不为零，才用当前的数量取代上次的
            # 计算利润时利润值为零的情况：交易所资产更新失败；交易所深度更新失败；利润值真的为零；资产更新错误
            last_cny = total_cny  # 操作结束后，将当前的人民币（和比特币）数量赋值给last_cny，作为下一轮操作的利润统计参考值
            last_btc = total_btc

        # 获取当前市场价格
        price = 0
        for exchange in exchanges:
            if (exchange.ask == 0) or (exchange.bid == 0):
                continue
            price = exchange.bid
            break

        if isNomal() and (price != 0) and (profit != 0):  # 利润值为零，说明可能存在问题，不记录到数据库
            # flag记录利润来源，0表示好比特，1表示其他市场
            record.record_op(datetime, "profit", None, None, flag, retainDigiNum(profit, 6),
                             "total_cny: %.6f, total_btc: %.6f, price: %.2f" % (total_cny, total_btc, price))

        print("%s: Current total_cny: %f, total_btc: %f; profit: %f" % (datetime, total_cny, total_btc, profit))
    print("****************************************")


def balanceFund():
    if (int(time.time() + 28800) % 86400 % int(24 / config.TIMES_BALANCEFUND * 3600) < config.TiggerTime) \
            and isNomal():
        # 撤销非好比特市场的所有订单，因为行情大波动时，实际滑点大于预估滑点，导致订单长时间地挂在不能成交的位置
        cancelAll()
        # 用来平衡市场的比特币和人民币价值，使之保持平衡
        global total_btc, total_cny, init_btc, last_btc, last_cny, exchanges
        ask = 0

        for exchange in exchanges:
            if (exchange.ask == 0) or (exchange.bid == 0):
                continue
            ask = exchange.ask
            break

        referPrice = ask  # 参考价格
        referBTC = (referPrice*total_btc + total_cny) / 2 / referPrice  # 比特币参考持仓
        fundBalance = referBTC - init_btc  # 需要平衡的币数，根据当前资产来算的
        fundDiff = fundBalance * referPrice  # 需要平衡的比特币数量的人民币市值

        # 资产差超过了资金触发阈值，并且参考价格不为零，并且当前市值跟上一轮差距小于阈值，才做资金平衡操作
        if (abs(fundDiff) > config.ThresholdPrice * total_btc) and (referPrice != 0) \
                and (abs((total_btc-last_btc)*referPrice+total_cny-last_cny) < init_btc * 20 * 1):
            if fundBalance > 0:  # 比特币跌了，留一定的缓冲区更低的时候买
                referBTC -= (config.ThresholdPrice * total_btc)/referPrice
            else:  # 比特币涨了，留一定的缓冲区更高的时候卖
                referBTC += (config.ThresholdPrice * total_btc)/referPrice

            init_btc = retainDigiNum(referBTC, 4)  # 用当前的持仓参考值覆盖之前的初始比特币数量

            balanceBtc()

            # 记录操作
            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
            record = BmobHelper()
            # 操作比特币数量记录在amount字段，正为买入比特币，负为卖出比特币；折合人民币记录在price字段
            record.record_op(datetime, "fundBalance", None, None, retainDigiNum(fundDiff, 4),
                             retainDigiNum(fundBalance, 4), None)


def onTick():
    global total_cny
    balanceBtc()


def main():
    global init_btc, total_btc, last_cny, total_cny, last_btc
    init_exchanges()
    updateCurrentDepth()
    updateCurrentAsset()
    if isNomal():
        last_cny = total_cny  # 记录程序运行前，人民币数量，给第一次onTick使用，每次onTick执行完后，last_cny都重新赋值
        last_btc = total_btc
        init_btc = total_btc  # 记录程序运行前，比特币的数量，此数量一直不变
        print("Init fund: BTC %.2f, CNY %.2f" % (total_btc, total_cny))
    else:
        print("Update asset failed! Init program failed! Please find the problems and restart.")
        sys.exit()
    cancelAll()
    while True:
        datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
        print('------' + datetime + '------')

        onTick()
        statisticsProfit()
        balanceFund()
        time.sleep(random.randint(4, int(config.TickInterval * 10))/10)


if __name__ == '__main__':
    main()
